Nasza aplikacja z pierwszego modułu była niezwykle prymitywna, więc teraz trochę ją rozwiniemy. Przy okazji poznamy kolejne możliwości oferowane przez Express.
Serwowanie plików statycznych
Zacznijmy od rozwinięcia treści naszych stron. Sam nagłówek to raczej za mało, prawda? Dodamy więc trochę tekstu do odpowiedzi każdego z endpointów.
W tym miejscu jednak od razu w głowie zapala się czerwona lampka. Dopóki zwracaliśmy sam nagłówek, spokojnie mogliśmy go trzymać bezpośrednio w pliku server.js. Przechowywanie tam większej treści nie ma jednak większego sensu – skrypt niepotrzebnie by się rozrósł, edycja plików stałaby się koszmarem, a ponadto każda ze stron powinna mieć przecież całą semantyczną strukturę.
Na szczęście Express zadbał o to, by nam pomóc. Treść HTML możemy trzymać w zewnętrznych plikach, a następnie zwrócić je pod danym endpointem za pomocą przyjaznej metody sendFile.
Jej użycie jest następujące:
app.get('link', (req, res) => {
res.sendFile('filename-path');
});
To zaskakująco proste, prawda?
Rozwijamy podstrony
Czas wykorzystać nową wiedzę w praktyce!
Zacznij od stworzenia katalogu views – to tam będziemy trzymać nasze pliki HTML. Oczywiście nazwa folderu jest dowolna. To, gdzie Express będzie szukał danego pliku, zależy po prostu od ścieżki, którą wskażemy.
mkdir views
Następnie utwórz pięć nowych plików, po jednym dla każdego endpointu.
touch views/index.html views/about.html views/contact.html views/info.html views/history.html
Teraz wypełnij je następującą treścią:
index.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Home</title>
</head>
<body>
<h1>Home</h1>
<p>Welcome to my page!</p>
</body>
</html>
about.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>About</title>
</head>
<body>
<h1>About</h1>
<p>I'm a very skillful programmer. Nice to meet you.</p>
</body>
</html>
contact.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Contact</title>
</head>
<body>
<h1>Contact</h1>
<p>Send me an email at programmer@example.com</p>
</body>
</html>
info.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Info</title>
</head>
<body>
<h1>Info</h1>
<p>I will put here all my accomplishments. Stay tuned!</p>
</body>
</html>
history.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>History</title>
</head>
<body>
<h1>History</h1>
<p>Page not yet available... Give me more time, folks.</p>
</body>
</html>
Nadszedł czas na odpowiednie zmiany w server.js. Nie chcemy już zwracać samych nagłówków, teraz będą to całe pliki!
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.sendFile('./views/index.html');
});
app.get('/about', (req, res) => {
res.sendFile('./views/about.html');
});
app.get('/contact', (req, res) => {
res.sendFile('./views/contact.html');
});
app.get('/info', (req, res) => {
res.sendFile('./views/info.html');
});
app.get('/history', (req, res) => {
res.sendFile('./views/history.html');
});
app.listen(8000, () => {
console.log('Server is running on port: 8000');
});
I... nie działa... Serwer, zamiast treści zaczął wyrzucać błąd – path must be absolute or specify root to res.sendFile. O co chodzi?
To, że serwer został uruchomiony w Twoim katalogu projektu, wcale nie oznacza, że chcesz, aby szukał plików właśnie tam. Express wymaga od nas ustalenia dokładnej ścieżki do pliku. Co mamy tutaj na myśli? Jeśli Twój folder projektu to np. C:\Kodilla\Express, to dokładną ścieżką do index.html byłoby C:\Kodilla\Express\views\index.html, a dla about.html – C:\Kodilla\Express\views\about.html.
Czy to oznacza, że musimy wpisywać aż tak długie ścieżki? Przecież nie byłoby to dobrym pomysłem. Zwykła zmiana lokalizacji naszego projektu powodowałaby potrzebę modyfikacji każdego z linków... Po raz kolejny Express nie zostawia nas bez pomocy i proponuje użycie wbudowanego w Node'a modułu path. Pamiętasz go? Był już przez nas używany.
const express = require('express');
const path = require('path');
const app = express();
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '/views/index.html'));
});
...
Jak to działa?
path.join(__dirname, '/views/index.html')
Metoda path.join stara się odpowiednio "skleić" ścieżkę bazową ze ścieżką docelową. Np. path.join('C:/Kodilla/Test' + '/views/index.html) powinno dać nam w rezultacie C:/Kodilla/Test/views/index.html. Natomiast stała __dirname zwraca adres aktualnej ścieżki. Dzięki niej nie będziemy musieli wpisywać, że skrypt aktualnie znajduje się np. w lokalizacji C:\Kodilla\Test, tylko ustali to za każdym razem sam Node.js. W takiej sytuacji zmiana lokalizacji projektu nie będzie nam straszna. Node po prostu zwróci pod __dirname nową ścieżkę i skrypt wciąż będzie działał.
Powyższy kod możemy więc rozumieć jako rozkaz: znajdź plik index.html w folderze views, przy czym zacznij szukać w katalogu, w którym odpalono skrypt.
Wyposażeni w nową wiedzę możemy zmodyfikować nasz kod.
const express = require('express');
const path = require('path');
const app = express();
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '/views/index.html'));
});
app.get('/about', (req, res) => {
res.sendFile(path.join(__dirname, '/views/about.html'));
});
app.get('/contact', (req, res) => {
res.sendFile(path.join(__dirname, '/views/contact.html'));
});
app.get('/info', (req, res) => {
res.sendFile(path.join(__dirname, '/views/info.html'));
});
app.get('/history', (req, res) => {
res.sendFile(path.join(__dirname, '/views/history.html'));
});
app.listen(8000, () => {
console.log('Server is running on port: 8000');
});
Czy nie ma jakiejś prostszej drogi? Nie moglibyśmy zdefiniować wszystkiego raz, globalnie?
Owszem, istnieje taka możliwość, ale będzie nam potrzebna funkcjonalność middleware'u. Opowiemy o niej dosłownie za chwilę.
Na razie sprawdź, czy skrypt zaczął ponownie działać. Powinien dać nam następujący efekt:
Middleware
Czym jest middleware? Najprościej mówiąc, to funkcjonalność pośrednicząca, która "wpycha się" pomiędzy rozpoczęcie a zakończenie jakiegoś procesu. Gdy poznawaliśmy Reduksa, mówiliśmy o Redux Thunk. On też pełnił właśnie taką rolę pośrednicząca. Wchodził pomiędzy uruchomienie funkcji w komponencie a dispatchowanie akcji i pozwalał na oddalenie tej operacji w czasie (pomagało nam to głównie przy wywołaniach asynchronicznych).
Żeby łatwiej było Ci zrozumieć, o czym mowa, zaczniemy od przykładu.
Powiedzmy, że zbudowaliśmy aplikację sklepu internetowego. Nie jest ona bardzo zaawansowana, ale posiada kilka podstron:
/– strona główna, dostępna dla wszystkich.
/cart – strona koszyka, również widoczna dla każdego.
/admin/products – strona z katalogiem produktów, dostępna tylko dla admina.
/admin/payments – strona z rachunkami, również widoczna tylko dla admina.
Pomysł jest oczywiście bardzo prosty. Niektóre podstrony są przygotowane dla klientów, pozostałe służą administrowaniu serwisem i powinny być dostępne tylko dla zalogowanych właścicieli. Jak mógłby wyglądać kod serwera takiej strony?
Wykorzystując naszą dotychczasową wiedzę, na pewno zbudowalibyśmy coś takiego:
const express = require('express');
const path = require('path');
const app = express();
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '/views/home.html'));
});
app.get('/cart', (req, res) => {
res.sendFile(path.join(__dirname, '/views/cart.html'));
});
app.get('/admin/products', (req, res) => {
if(isAdmin()) res.sendFile(path.join(__dirname, '/views/admin/products.html'));
else res.send('Go away!');
});
app.get('/admin/payments', (req, res) => {
if(isAdmin()) res.sendFile(path.join(__dirname, '/views/admin/payments.html'));
else res.send('Go away!');
});
...
Oczywiście funkcja isAdmin() jest umowna. Załóżmy, że sprawdza ona, czy użytkownik jest administratorem i zwraca true (jeśli tak) lub false (jeśli nie).
Czy ten kod ma sens? Oczywiście. Widzisz jednak, że podobnie jak w poprzednim rozdziale, mamy problem z powtarzalnością. W każdym endpoincie /admin/... sprawdzamy, czy użytkownik jest administratorem. Nie jest to przeszkodą, póki mamy ich niewiele, ale dla większej liczby mogłoby stać się bardziej uciążliwe.
Nie da się jakoś tego skrócić? W końcu widać, że endpointy "adminowe" mają część wspólną – przedrostek /admin. Nie można by więc w jakiś sposób zmusić Expressa, aby sprawdzał rolę użytkownika dla każdego linku rozpoczynającego się w taki sposób? Odpowiedź jest pozytywna – da się!
Metoda app.use
Wyjściem jest oczywiście wcześniej wspomniany pomysł z middleware. Express dostarcza nam specjalną metodę o nazwie use, która potrafi "wepchnąć się" pomiędzy otrzymane od klienta żądania a zwrócenie odpowiedzi i wykonać jakieś dodatkowe operacje. Co ważne, sami wybieramy, w jakiej sytuacji mają się one wykonywać.
Możemy więc przed zwróceniem odpowiedzi pobrać coś z bazy danych albo sprawić, że każde żądanie przeszuka pliki w __dirname. Będziemy mogli również ustawiać operacje tylko dla wybranej grupy endpointów i jest to coś, czego potrzebujemy w przykładzie ze sklepem. Ustalimy, że wszystkie endpointy rozpoczynające się na /admin uruchomią najpierw funkcję sprawdzenia roli, i dopiero gdy potwierdzimy, że użytkownik jest administratorem, middleware uruchomi instrukcję z endpointu.
Podsumujmy więc, jak wyglądałoby to dokładnie w przypadku naszego sklepu.
Obecna sytuacja prezentuje się następująco:
Proces jest bardzo prosty. Użytkownik wysyła request, serwer szuka odpowiedniego endpointu i zwraca response.
Jak będzie wyglądał ten proces po zmianach i wprowadzeniu middleware'u?
Wciąż po requeście nastąpi response, ale pomiędzy nimi serwer uruchomi nasz middleware, który w założeniu sprawdzi, czy link nie jest przypadkiem jednym z "adminowych". Jeśli tak, wtedy ustali, czy jesteśmy zalogowani i zdecyduje o przekierowaniu nas do endpointu admin/payments lub admin/products, albo po prostu powie "Go away!".
Oczywiście to, co znajdzie się w middleware, zależy tylko i wyłącznie od nas. Na dobrą sprawę, jesteśmy w stanie zrobić tam wszystko. Moglibyśmy nawet ustalić, że nie ważne, jaki link użytkownik wybrał, jego endpoint i tak będzie losowy.
Podsumowując, middleware w Expressie to po prostu pośrednik pomiędzy rozpoczęciem żądania a zwróceniem odpowiedzi klientowi.
Jeśli czujesz, że nie do końca jeszcze rozumiesz cały zamysł, nie przejmuj się. Gdy tylko wykorzystamy middleware w praktyce, przekonasz się, że jest stosunkowo łatwy w użyciu.
Czas na praktykę!
Jak to wszystko wyglądałoby w kodzie?
Metody .use używa się następująco:
app.use(path, (req, res, next) => {
next();
});
Parametr path to ścieżka, którą chcemy "wychwycić". Ustalenie tu np. wartości /admin informowałoby, że wybieramy requesty zaczynające się właśnie takim przedrostkiem. Gdybyśmy wstawili /, wtedy pod uwagę brane byłyby wszystkie linki (bo w końcu każdy zaczyna się na /). W sytuacji, gdy zależy nam na wszystkich requestach, nie musimy pisać nawet tej wartości. Wystarczy, że zrezygnujemy z tego parametru w ogóle:
app.use((req, res, next) => {
next();
});
Funkcja callback, którą widzisz dalej, to po prostu instrukcje, które mają się wykonać, jeśli Express uzna, że dany request pasuje (czyli, że np. faktycznie zaczyna się od /admin).
(req, res, next) => {
next();
});
Pojawiają się tutaj aż trzy parametry. req i res to po prostu obiekt żądania i odpowiedzi. Mamy do nich taki sam dostęp jak w endpointach. To ważna opcja, bo dzięki temu możemy zwrócić coś klientowi już na tym etapie i w ogóle nie uruchamiać kodu spod samego endpointu.
Na przykład:
app.use('/forbidden-area', (req, res, next) => {
res.send('Go away!');
});
Pozostaje jeszcze parametr next, który – jak widać na przykładzie – jest jakąś funkcją. Jaką dokładnie? Taką, która w normalnych warunkach, bez middleware, byłaby wykonywana pod tym linkiem.
Spójrzmy na taki kod:
app.use('/home', (req, res, next) => {
console.log('Hello!');
next();
});
W przypadku linku /home skrypt powoduje pokazanie w konsoli tekstu Hello. Następnie serwer rusza dalej i przeszukuje plik w celu znalezieniu samego konkretnego endpointu.
Co stałoby się, gdybyśmy jednak nie użyli tej funkcji?
app.use('/home', (req, res, next) => {
console.log('Hello!');
});
W konsoli zobaczylibyśmy tekst Hello i na tym etapie serwer zakończyłby poszukiwania. Nawet gdyby istniał konkretny pasujący endpoint i znajdował się dalej w kodzie, serwer i tak by go nie uruchomił.
Jak widzisz, funkcja next daje nam kontrolę nad dalszym działaniem serwera. To bardzo przydatna opcja.
Aplikacja sklepu po zmianach
Jak wyglądałby kod naszego przykładowego sklepu po zmianach?
const express = require('express');
const path = require('path');
const app = express();
app.use('/admin', (req, res, next) => {
if(isAdmin()) next();
else res.send('Go away!');
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '/views/home.html'));
});
app.get('/cart', (req, res) => {
res.sendFile(path.join(__dirname, '/views/cart.html'));
});
app.get('/admin/products', (req, res) => {
res.sendFile(path.join(__dirname, '/views/admin/products.html'));
});
app.get('/admin/payments', (req, res) => {
res.sendFile(path.join(__dirname, '/views/admin/payments.html'));
});
Nas interesuje zwłaszcza ten kawałek:
app.use('/admin', (req, res, next) => {
if(isAdmin()) next();
else res.send('Go away!');
});
Jak on działa? Pewnie już się domyślasz. Nasz middleware "wyłapuje" wszystkie żądania, w których link zaczyna się na /admin (czyli np. admin/payments). Następnie, jeśli na taki link trafi, sprawdza, czy jesteśmy administratorem. Jeśli tak, pozwala serwerowi działać dalej i ten spokojnie próbuje znaleźć już konkretny endpoint. Jeśli jednak nie, kończymy odpowiedź, zwracając klientowi tekst Go away!.
Co nam to dało? Teraz w endpointach "adminowych" nie musimy już sprawdzać roli użytkownika. Mamy pewność, że skoro serwer je odpala, wcześniej poprawnie przeszliśmy proces sprawdzenia w middleware.
Powrót do pracy
Wiemy już jak działa middleware, do czego i w jaki sposób możemy wykorzystać use. Czas więc wrócić do naszej aplikacji i spróbować naprawić to, co nam nie pasuje.
Chcemy w jakiś sposób zniwelować potrzebę ciągłego odwoływania się do __dirname. Oczywiście pomoże nam w tym middleware.
app.use((req, res, next) => {
res.show = (name) => {
res.sendFile(path.join(__dirname, `/views/${name}`));
};
next();
});
Spójrz na powyższy kod. Zacznijmy od ustalenia, na jakie requesty zadziała. Skoro nie podano żadnej ścieżki, bierzemy pod uwagę wszystkie linki. Nieważne, czy wejdziemy pod /, contact czy about – na pewno ten middleware się uruchomi.
Co dokładnie się w nim dzieje?
res.show = (name) => {
res.sendFile(path.join(__dirname, `/views/${name}`));
};
Przede wszystkim dodajemy do obiektu res nową metodę! Ma ona przyjmować nazwę pliku, a następnie zwracać go za pomocą sendFile. Co ważne jednak dobiera się do niego przy użyciu dokładnej ścieżki. Otrzymujemy więc funkcję, która potrzebuje jedynie nazwy pliku, a daje nam to, co wcześniej osiągaliśmy znacznie dłuższym kodem.
Przykładowo res.show('about.html') powinno zwracać klientowi about.html. Jest to znacznie łatwiejszy zapis, a ma taki sam efekt jak res.sendFile(path.join(__dirname, '/views/about.html'));.
Oczywiście next służy do tego, co zawsze – pozwala serwerowi ruszyć dalej w poszukiwaniu endpointu. Co ważne, gdy już go znajdzie, nasza nowa metoda show stanie się dostępna w obiekcie res i będziemy mogli wykorzystać ją zamiast sendFile!
Nasz kod zmieni się więc następująco:
const express = require('express');
const path = require('path');
const app = express();
app.use((req, res, next) => {
res.show = (name) => {
res.sendFile(path.join(__dirname, `/views/${name}`));
};
next();
});
app.get('/', (req, res) => {
res.show('index.html');
});
app.get('/about', (req, res) => {
res.show('about.html');
});
app.get('/contact', (req, res) => {
res.show('contact.html');
});
app.get('/info', (req, res) => {
res.show('info.html');
});
app.get('/history', (req, res) => {
res.show('history.html');
});
app.listen(8000, () => {
console.log('Server is running on port: 8000');
});
Skąd mamy pewność, że na etapie znalezienia endpointu, funkcja res.show będzie już przygotowana? Nasz middleware jest w kodzie przed endpointami, więc wykona się jako pierwszy. Dodatkowo wyłapujemy wszystkie możliwe linki, zatem nie musimy się bać, że któryś endpoint nie będzie miał dostępu do tej metody.
Wyłapujemy wadliwe linki
Co ciekawe, metoda use może przydać się też do odpowiedniego obsłużenia niepoprawnych requestów! Wystarczy, że wstawimy nasz middleware po endpointach:
...
app.get('/info', (req, res) => {
res.show('info.html');
});
app.get('/history', (req, res) => {
res.show('history.html');
});
app.use((req, res) => {
res.status(404).send('404 not found...');
})
Jak to działa? Jeśli wpiszemy taki link, który rzeczywiście ma pasujący endpoint na serwerze, klient zobaczy właściwą odpowiedź (plik HTML), a serwer skończy na tym etapie proces poszukiwania. Mimo tego, że middleware na końcu nie ma podanej ścieżki i jest w stanie wyłapać każdy request, zwyczajnie nigdy nie zostanie nawet uruchomiony.
Jeśli jednak serwer nie znajdzie pasującego endpointu, to w końcu trafi do middleware i pokaże klientowi odpowiedź 404 not found. Tym samym stworzyliśmy funkcjonalność wyłapującą wszystkie niepoprawne zapytania. Jeśli ktoś wpisze /info, to zwrócimy mu treść pliku info.html, ale jeśli poda link, którego nie obsługujemy – zobaczy przygotowany przez nas komunikat. Na pewno będzie wyglądał on lepiej niż domyślne Cannot GET /.
Kody odpowiedzi
Idea kodów odpowiedzi nie jest Ci zapewne obca. Była już o nich mowa w module dotyczącym AJAX-u i API. Pamiętasz kod 404 zwracany w przypadku braku zasobu lub błędnego linku albo 500 informujący o błędzie serwera? Właśnie o tym mowa.
Kiedy sami programujemy serwer, decydujemy jaki kod odpowiedzi zwrócimy klientowi – będzie to 200 sugerujące, że wszystko gra, czy może coś innego. W przykładzie powyżej zwracamy klientowi kod 404, informujący o błędnej ścieżce. Stąd też użycie wbudowanej w Express funkcji status.
Dlaczego nie ustawialiśmy takiego kodu do wcześniejszych endpointów? Ponieważ tam odpowiedź była jak najbardziej zgodna z oczekiwaniami klienta, a kodem pasującym byłby 200. Jest on domyślną odpowiedzią obiektu response, o ile nie ustalimy inaczej.
Swoją drogą, na pewno udało Ci się zauważyć, że tym razem brakuje funkcji next. Pominęliśmy ją z prostego powodu. Nie ma sensu odpalania kolejnej funkcji, skoro nasz endpoint jest już ostatnim elementem aplikacji.
To trochę inny przykład wykorzystania middleware'u, ale jak widzisz równie przydatny.
Nasza aplikacja powinna wyglądać teraz następująco:
const express = require('express');
const path = require('path');
const app = express();
app.use((req, res, next) => {
res.show = (name) => {
res.sendFile(path.join(__dirname, `/views/${name}`));
};
next();
});
app.get('/', (req, res) => {
res.show('index.html');
});
app.get('/about', (req, res) => {
res.show('about.html');
});
app.get('/contact', (req, res) => {
res.show('contact.html');
});
app.get('/info', (req, res) => {
res.show('info.html');
});
app.get('/history', (req, res) => {
res.show('history.html');
});
app.use((req, res) => {
res.status(404).send('404 not found...');
})
app.listen(8000, () => {
console.log('Server is running on port: 8000');
});
next(), nie tylko w middleware
Dotychczas pokazywaliśmy użycie next wyłącznie przy wykorzystaniu funkcji use. Warto wiedzieć, że tego parametru możemy używać również w samych endpointach.
app.get('/about', (req, res, next) => {
res.send('About me');
next();
})
app.use((req, res) => {
})
Kod powyżej zadziałaby tak, że wejście w link /about nie tylko pokazałoby treść About me, ale kazałoby jeszcze działać serwerowi dalej. Ten szukałby więc kolejnego pasującego endpointu i trafiłby w końcu do... naszego middleware na samym dole.
Jak można zastosować ten pomysł? Moglibyśmy np. na koniec requestu czyścić w ten sposób jakieś dane albo rozłączać się z bazą danych.
Dodajemy zewnętrzne zasoby
Do tej pory nasze podstrony były niezwykle proste. Co się jednak stanie, gdy zaczniemy tworzyć o wiele bardziej skomplikowane witryny, np. z rozbudowanym CSS i wykorzystaniem obrazków? Jak Express poradzi sobie z dużymi plikami HTML? Sprawdźmy!
Do celów testowych edytujemy plik index.html. Obecnie wygląda on tak:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Home</title>
</head>
<body>
<h1>Home</h1>
<p>Welcome to my page!</p>
</body>
</html>
Dodamy do niego linkowanie arkusza stylów oraz obrazka.
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>Home</h1>
<p>Welcome to my page!</p>
<img src="test.png">
</body>
</html>
Nic specjalnego, ale jak widzisz, linkujemy do kolejnych zasobów. Wydaje się, że to jak najbardziej sensowne.
Teraz czas na stworzenie tych plików. Dodaj do folderu projektu style.css z następującą zawartością:
h1 {
font-weight: lighter;
font-style: italic;
}
Następnie w tym samym folderze umieść dowolny obrazek o nazwie test.png.
Powinno działać? Zobaczymy! Zrestartuj serwer i sprawdź.
Okazuje się, że nie działa ani obrazek, ani arkusz... Sprawdźmy narzędzia developerskie i zakładkę Network.
Nasze zasoby nie zostały znalezione, a serwer zwrócił kod błędu 404. Możesz wpisać ścieżkę do pliku bezpośrednio w przeglądarce (http://localhost/style.css), a i tak dostaniesz informację, że ona nie istnieje.
Dlaczego? Przecież wydaje się, że podaliśmy prawidłowe nazwy pliku... Same zasoby też są obecne na dysku w tym samym folderze, w którym uruchamiamy serwer.
Odpowiedź jest prosta. To, że serwer jest uruchamiany w jakimś folderze nie oznacza, że automatycznie daje klientowi dostęp do plików w nim obecnych. Wbrew pozorom, jest to bardzo dobra informacja. Jeśli byłoby inaczej, równie dobrze, jak plik style.css, klient mógłby otworzyć server.js – wystarczyłoby wejść w link http://localhost/server.js. Tego byśmy nie chcieli, bo przecież często są tam informacje poufne, jak na przykład hasła do bazy danych, wykorzystywane przy połączeniu.
Oczywiście serwer ma wgląd do tych plików, jednak domyślnie nie udostępnia ich klientom.
Możemy jednak stworzyć odpowiednie endpointy i ustalić np. że link http://localhost/style.css faktycznie będzie zwracał plik style.css. Tym właśnie zaraz się zajmiemy!
Warto zapamiętać
Utworzenie serwera w danym folderze nie daje klientowi automatycznie dostępu do plików w nim zawartych. Serwer udostępnia tylko takie ścieżki (endpointy), jakie sami w nim podamy.
Przygotowujemy obsługę zasobów
Do dzieła!
W tym momencie obsługujemy następujące endpointy:
/
/about
/contact
/info
/history
Gdy wpiszemy jakikolwiek inny link, serwer zwróci nam komunikat 404 not found..., o co dba nasz middleware na końcu:
app.use((req, res) => {
res.status(404).send('404 not found...');
})
Zatem, co dzieje się, kiedy chcemy, aby nasz serwer połączył się z plikiem http://localhost:8000/style.css albo http://localhost:8000/test.png? Otrzymujemy właśnie taki komunikat.
Teraz już wiemy, w czym rzecz. Skoro serwer nie może znaleźć endpointu style.css, to trafia do middleware na końcu i zwraca komunikat. Tak samo sytuacja ma się z test.png.
Musimy więc po prostu przygotować te endpointy:
app.get('/style.css', (req, res) => {
res.sendFile(path.join(__dirname, '/style.css'));
});
app.get('/test.png', (req, res) => {
res.sendFile(path.join(__dirname, '/test.png'));
});
Od tego momentu, pod /style.css serwer będzie faktycznie zwracał nasz plik style.css, a pod /test.png obrazek test.png. Oczywiście równie dobrze endpoint /style.css mógłby kierować np. do pliku o nazwie main.css. To w końcu my decydujemy o tym, co pokaże się pod danym linkiem.
Wszystko już działa i jak widzisz nie było to takie trudne. Zapewne zdajesz sobie jednak sprawę, że to rozwiązanie bardzo czasochłonne. Często używamy o wiele większej ilości plików zewnętrznych, a ustawianie dla każdego z osobna nowego endpointu nie byłoby najlepszym pomysłem.
Na szczęście wiesz już, że w takich sytuacjach może nam pomóc middleware!
Wbudowany middleware express.static
Co najlepsze, nie musimy nawet pisać go sami. Express daje kilka gotowych funkcji middleware, odpowiadających na najważniejsze potrzeby. Jedną z nich jest właśnie express.static, która pozwala udostępniać całe foldery.
Wystarczyłoby więc dodać taki kod:
app.use(express.static(__dirname));
...a każdy link style.css, test.png, czy nawet package.json, byłby zwracany poprawnie przez serwer! W ten sposób udostępniamy całą zawartość katalogu naszego projektu, bowiem __dirname to jego ścieżka.
Oczywiście nie jest to najlepszym rozwiązaniem, bo oddajemy w ten sposób również dostęp do server.js. Dlatego częściej tworzy się i udostępnia jedynie specjalny katalog na pliki, które będą wykorzystywane przy renderowaniu widoku. Folder ten można nazwać np. public.
app.use(express.static(path.join(__dirname, '/public')));
Jak express.static działa "pod maską"?
Tak naprawdę dość prosto. Jego praca jest równoznaczna z ręcznym dodawaniem endpointów do plików, przy czym nazwa ścieżki jest dokładnie taka sama, jak ścieżka do pliku względem udostępnianego folderu.
Np. dla katalogu public z taką zawartością:
style.css
script.js
/images
logo.png
app.use(express.static(path.join(__dirname, '/public'))); spowodowałoby obsługę następujących endpointów:
http://localhost:8000/style.css
http://localhost:8000/script.js
http://localhost:8000/images/logo.png
W ramach ćwiczenia spróbuj teraz dokończyć naszą aplikację, a więc:
- Stwórz folder
public.
- Przenieś tam pliki
test.png i style.css.
- Usuń konkretne endpointy dla tych plików.
- Dodaj middleware udostępniający cały katalog
public. Oczywiście należy użyć tutaj wbudowanego express.static.
Pokaż gotowy kod
Ukryj gotowy kod
const express = require('express');
const path = require('path');
const app = express();
app.use((req, res, next) => {
res.show = (name) => {
res.sendFile(path.join(__dirname, `/views/${name}`));
};
next();
});
app.use(express.static(path.join(__dirname, '/public')));
app.get('/', (req, res) => {
res.show('index.html');
});
app.get('/about', (req, res) => {
res.show('about.html');
});
app.get('/contact', (req, res) => {
res.show('contact.html');
});
app.get('/info', (req, res) => {
res.show('info.html');
});
app.get('/history', (req, res, next) => {
res.show('history.html');
});
app.use((req, res) => {
res.status(404).send('404 not found...');
})
app.listen(8000, () => {
console.log('Server is running on port: 8000');
});
Posumowanie
Nasza aplikacja prawie nie zmieniła się z wyglądu. Nowością jest jedynie pokazywanie własnego komunikatu o błędzie w przypadku wejścia na nieistniejącą stronę i trochę więcej informacji. Modyfikacji dokonaliśmy natomiast "pod maską" – wykorzystaliśmy zewnętrzne pliki do wyświetlania treści, poznaliśmy koncepcję middleware i wykorzystaliśmy ją w praktyce. Mimo nowości skrypt mocno się nie skomplikował. Zwróć uwagę, że nasz serwer to zaledwie 40 linijek kodu.